package com.gateway.handler;

import static org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping.ManagementPortType.DIFFERENT;
import static org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping.ManagementPortType.DISABLED;
import static org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping.ManagementPortType.SAME;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import org.springframework.cloud.gateway.handler.FilteringWebHandler;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.core.env.Environment;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author xiangminwen
 * @date 2025/2/21 14:09
 */
public class CustomRoutePredicateHandler extends RoutePredicateHandlerMapping {

    private final FilteringWebHandler webHandler;

    private final RouteLocator routeLocator;

    private final Integer managementPort;

    private final RoutePredicateHandlerMapping.ManagementPortType managementPortType;

    Cache<String, String> CACHE = Caffeine.newBuilder().initialCapacity(100)
            .expireAfterWrite(10, TimeUnit.MINUTES).softValues()
            .maximumSize(1000).build();

    public CustomRoutePredicateHandler(FilteringWebHandler webHandler,
            RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties,
            Environment environment) {
        super(webHandler, routeLocator, globalCorsProperties, environment);
        this.webHandler = webHandler;
        this.routeLocator = routeLocator;

        this.managementPort = getPortProperty(environment, "management.server.");
        this.managementPortType = getManagementPortType(environment);
        setOrder(1);
        setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
    }

    private RoutePredicateHandlerMapping.ManagementPortType getManagementPortType(Environment environment) {
        Integer serverPort = getPortProperty(environment, "server.");
        if (this.managementPort != null && this.managementPort < 0) {
            return DISABLED;
        }
        return ((this.managementPort == null
                || (serverPort == null && this.managementPort.equals(8080))
                || (this.managementPort != 0 && this.managementPort.equals(serverPort)))
                ? SAME : DIFFERENT);
    }

    private static Integer getPortProperty(Environment environment, String prefix) {
        return environment.getProperty(prefix + "port", Integer.class);
    }

    @Override
    protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
        // don't handle requests on management port if set and different than server port
        if (this.managementPortType == DIFFERENT && this.managementPort != null
                && exchange.getRequest().getURI().getPort() == this.managementPort) {
            return Mono.empty();
        }
        exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
        Mono<Route> routeMono = lookupRoute(exchange);

        return routeMono.flatMap((Function<Route, Mono<?>>) r -> {
            exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (logger.isDebugEnabled()) {
                logger.debug(
                        "Mapping [" + getExchangeDesc(exchange) + "] to " + r);
            }

            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
            return Mono.just(webHandler);
        }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
            exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (logger.isTraceEnabled()) {
                logger.trace("No RouteDefinition found for ["
                        + getExchangeDesc(exchange) + "]");
            }
        })));
    }

    @Override
    protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) {
        return super.getCorsConfiguration(handler, exchange);
    }

    private String getExchangeDesc(ServerWebExchange exchange) {
        StringBuilder out = new StringBuilder();
        out.append("Exchange: ");
        out.append(exchange.getRequest().getMethod());
        out.append(" ");
        out.append(exchange.getRequest().getURI());
        return out.toString();
    }

    protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
        String path = exchange.getRequest().getURI().getPath();
        String routeId = CACHE.getIfPresent(exchange.getRequest().getURI().getPath());
        if (Objects.nonNull(routeId)) {
            return this.routeLocator.getRoutes().filter(route -> route.getId().equals(routeId)).next();
        }
        return this.routeLocator.getRoutes()
                .concatMap(route -> Mono.just(route).filterWhen(r -> {
                            exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
                            return r.getPredicate().apply(exchange);
                        })
                        .doOnError(e -> logger.error(
                                "Error applying predicate for route: " + route.getId(),
                                e))
                        .onErrorResume(e -> Mono.empty()))
                .next()
                .map(route -> {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Route matched: " + route.getId());
                    }
                    if (Objects.isNull(routeId)) {
                        CACHE.put(path, route.getId());
                    }
                    validateRoute(route, exchange);
                    return route;
                });
    }

    /**
     * Validate the given handler against the current request.
     * <p>
     * The default implementation is empty. Can be overridden in subclasses, for example
     * to enforce specific preconditions expressed in URL mappings.
     * @param route    the Route object to validate
     * @param exchange current exchange
     * @throws Exception if validation failed
     */
    @SuppressWarnings("UnusedParameters")
    protected void validateRoute(Route route, ServerWebExchange exchange) {
    }

    protected String getSimpleName() {
        return "RoutePredicateHandlerMapping";
    }

}
